<?php
/**
 * 优化调试框架, 输出调试变量以及优化相关的步耗时总耗时等相关信息
 * Author : Drunk
 * Website : drunkce.com, idrunk.net
 * Date : 2016-06-19 19:53
 */

namespace drunk;

class test{
    /**
     * test constructor.
     * @param int $bit {&1:输出未格式化源码, &2:输出不挂起, &4:计次调试}
     */
    function __construct($bit = 0){
        $this->bit = $bit;
    }

    /**
     * 点位标记
     * @param array $debug_backtrace
     * @param int $bit {&1:输出json, 2:输出PHP代码}
     */
    public function mark($debug_backtrace, $bit = 0){
        if(true === $this->_step_count_not_on()) return $this; // 如果设置了计次输出却未开启则不作处理
        if(empty($debug_backtrace)) $debug_backtrace = [[]];
        $time_request = ceil($_SERVER['REQUEST_TIME_FLOAT'] * 1000); // 请求时间时间戳
        $point = current($debug_backtrace);
        $point['bit'] = $bit; // 怎么输出
        $point['number'] = 1; // 标记点统计序号
        $point['time'] = ceil(microtime(1) * 1000); // 当前时间戳
        $point['filename'] = basename($point['file'], '.php'); // 文件名
        $point['time_total'] = floor(($point['time'] - $time_request) / 100) / 10; // 总耗秒
        $points = $this->_points();
        if(empty($points)){
            $point['time_prev'] = $time_request; // 前步时间戳
        }else{
            $point['number'] = count($points) + 1; // 标记点统计序号
            $point['time_prev'] = $points[$point['number'] - 2]['time'];
        }
        $point['time_micro_step'] = $point['time'] - $point['time_prev']; // 步耗毫秒
        $point['time_step'] = floor($point['time_micro_step'] / 100) / 10; // 步耗秒
        $this->_points($point);
        return $this;
    }

    /**
     * 判断是否计次断点输出却未开启
     * @return bool|int {int : 计次输出参数已开启, true : 是计次输出但参数未开启, false : 参数未开启}
     */
    private function _step_count_not_on(){
        $dtest = isset($_GET['dtest']) ? (int) $_GET['dtest'] : false;
        return ($this->bit & 4) && !$dtest ? true : $dtest; // 如果设置了计次输出却未开启
    }

    /**
     * 获取/记录点标记
     * @param array|null $point
     * @return mixed
     */
    private function _points($point = null){
        if( !isset($this->_hash) ) $this->_hash = 'test-points-'. uniqid();
        if( !isset($GLOBALS[$this->_hash]) ) $GLOBALS[$this->_hash] = [];
        return is_null($point)
            ? $GLOBALS[$this->_hash]
            : $GLOBALS[$this->_hash][] = $point;
    }

    /**
     * 点位标记调试信息处理输出
     * @param int $bit {&1:输出未格式化源码, &2:输出不挂起, &4:计次调试}
     */
    public function dump($bit = 0){
        $dtest = $this->_step_count_not_on();
        if(true === $dtest) return; // 如果设置了计次输出却未开启则不作处理
        if((int) $bit) $this->bit = $bit;
        $points = $this->_points();
        $tab = '';
        if($this->_is_src()){ // 是否以源码形式输出
            foreach(($points) as $k=>$v)
                $tab .= $this->_mark_handle_src($v);
            echo $tab; // 输出调试内容
        }else{
            $tab .= self::_css(); // 非以源码形式展示, 则加入css
            $tab .= '<table class="drunk-test"><thead><tr><th class="number">序号</th><th class="line">行号</th><th class="filename">文件名</th>
            <th class="time-micro-step">步耗毫秒</th><th class="time-step">步耗秒</th><th class="time-total">总耗秒</th><th></th></tr></thead>';
            foreach(($points) as $k=>$v)
                $tab .= $this->_mark_handle($v);
            $tab .= '</table>';
            $this->_show($tab); // 输出调试内容
        }
        if(!($this->bit & 2) && !$this->_step_count_less($dtest)) die; // 如果有未配置不挂起且已达计次输出阀或非计次循环, 则挂起
    }

    /**
     * 是否以源码形式展示
     */
    private function _is_src(){
        return $this->bit & 1;
    }

    /**
     * 计次数
     * @var int
     */
    private static $_counter = 0;

    /**
     * 是否计次输出
     * @param int $dtest {&1:输出未格式化源码, &2:输出不挂起, &4:计次调试}
     * @return bool|0 {0 : 是计次输出且已达输出阀, true : 是计次输出但未达输出阀, false : 非计次循环}
     */
    private function _step_count_less($dtest){
        if($dtest === false) return false;
        return ++self::$_counter >= $dtest
            ? 0
            : true;
    }

    /**
     * 点位标记调试信息处理
     * @param array $point
     * @return string
     */
    private function _mark_handle($point){
        $title = '序      号：'.$point['number'].'&#10;'.
            '行      号：'.$point['line'].'&#10;'.
            '文  件 名：'.$point['filename'].'&#10;'.
            '步耗毫秒：'.$point['time_micro_step'].'&#10;'.
            '步  耗 秒：'.$point['time_step'].'&#10;'.
            '总  耗 秒：'.$point['time_total'].'&#10;'.
            '文件路径：'.$point['file'];
        $tab = '<tbody><tr class="dt-head dt-head-'.($point['number']%2).'" title="'.$title.'"><th>'.$point['number'].'</th><th>'.$point['line'].'</th><th>'.$point['filename'].
            '</th><th>'.$point['time_micro_step'].'</th><th>'.$point['time_step'].'</th><th>'.$point['time_total'].'</th><th></th></tr>';
        foreach($point['args'] as $k=>$v){
            $title2 = '参      数：'.($k+1).'&#10;行      号：'.$point['line'].'&#10;文  件 名：'.$point['filename'];
            $content = htmlspecialchars(preg_replace('/\]\=\>\n(\s+)/m', '] => ', $this->_dump($v, (int) $point['bit'])), ENT_QUOTES); // 以源码形式展示
            $tab .= '<tr class="dt-arg dt-arg-'.($k%2).'" title="'.$title2.'"><th class="number">'.$point['number'].'-'.($k+1).'</th><td colspan="6"><pre contenteditable="true">'.
                $content.'</pre></td></tr>';
        }
        return $tab . '</tbody>';
    }

    /**
     * 点位标记调试信息处理
     * @param array $point
     * @return string
     */
    private function _mark_handle_src($point){
        $tab = '';
        foreach($point['args'] as $k=>$v)
            $tab .= $this->_dump($v, (int) $point['bit']) . "\n\n";
        return $tab;
    }

    /**
     * 变量解析处理
     * @param object $variable
     * @param int $bit_var {&1:输出json, &2:输出PHP代码}
     * @return string
     */
    private function _dump($variable, $bit_var = 0){
        if($bit_var & 1){
            $content = json_encode($variable);
        }elseif($bit_var & 2){
            $content = var_export($variable, true);
        }else{
            ob_start();
            var_dump($variable);
            $content = ob_get_clean();
        }
        return $content;
    }

    /**
     * @param $variable
     * @param $bit_var
     * @return string
     */
    public static function dump_code($variable, $bit_var = 0){
        $obj = new test();
        return htmlspecialchars(preg_replace('/\]\=\>\n(\s+)/m', '] => ', $obj->_dump($variable, $bit_var)), ENT_QUOTES);
    }

    /**
     * 输出调试内容
     * @param $content
     */
    private function _show($content){
        echo($this->_is_src() || ob_get_length()
            ? $content // 如果以源码形式展示或者已输出内容, 则直接返回内容
            : '<!doctype html><html xmlns="http://www.w3.org/1999/xhtml"><head><meta charset="utf-8" /><title>Drunk Debug</title><body>'."\n".$content."\n".'</body></html>');
    }

    /**
     * 加载非源码形式css样式
     * @return string
     */
    private static function _css(){
        if(!empty($GLOBALS['test_css_loaded'])) return '';
        $GLOBALS['test_css_loaded'] = 1;
        return'
<style type="text/css">
.drunk-test{border-collapse:collapse;font-size:12px;width:auto !important;}
.drunk-test thead th{background:#e9e9e9;padding:5px 10px;}
.drunk-test th.number{width:40px;}
.drunk-test th.line{width:40px;}
.drunk-test th.filename{width:256px;}
.drunk-test th.time-micro-step{width:60px;}
.drunk-test th.time-step{width:40px;}
.drunk-test th.time-total{width:40px;}
.drunk-test tbody tr:hover th,.drunk-test tbody tr:hover td{background:#fefefe;}
.drunk-test th,.drunk-test td{border:1px solid #e3e3e3;padding:3px 5px;}
.drunk-test .dt-head th{background:#eee;font-weight:400;}
.drunk-test .dt-head-1 th{background:#f2f2f2;}
.drunk-test .dt-arg th{border-color:#e9e9e9;background:#f3f3f3;font-weight:400;}
.drunk-test .dt-arg-1 th{background:#efefef;}
.drunk-test .dt-arg td{border-color:#eee;background:#fefefe;font-weight:400;text-align:left !important;}
.drunk-test .dt-arg-1 td{background:#fcfcfc;}
.drunk-test pre{line-height:20px;word-wrap:break-word;word-break:break-all;margin:0;max-height:400px;overflow-x:hidden !important;overflow-y:auto !important;}
</style>';
    }
}